﻿using System.IO.Compression;
using System.Text.Json;

namespace WinAutoUpdate {
  public static class HttpClientSingleton {
    private static readonly HttpClient fHttpClient = new();
    public static HttpClient Instance => fHttpClient;
  }

  internal class TAutoUpdateHelp {

    private const string fLogDir = "logs"; // 日志存放目录
    private const string fConfigName = "config.ini"; // 配置文件名称
    private List<Version>? fAllVersions = null;

    /// <summary>
    /// 检查并更新
    /// </summary>
    /// <returns></returns>
    internal async Task CheckAndUpdate() {
      try {
        fAllVersions = await GetAllVersions();
        if (fAllVersions != null) {
          var bLastestVersion = fAllVersions.OrderByDescending(v => v).FirstOrDefault();
          if (bLastestVersion > new Version(CurrentVersion)) {
            await UpdateToLatestVersion(CurrentVersion, bLastestVersion);
          }
        }
      }
      catch (Exception ex) {
        string bError = $"{ex.Message}";
#if DEBUG
        MessageBox.Show(bError, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
#endif
        LogMessage(bError); // 全部更新程序只在这里记录错误和提示信息，别的地方只抛出异常
      }
    }

    /// <summary>
    /// 读取配置文件中的版本号
    /// </summary>
    /// <remarks>不加访问修饰符，默认应用的访问修饰符是private</remarks>
    static string CurrentVersion {
      get {
        var bValue = GetConfigValue("Settings", "CurrentVersion") ?? "1.0.0"; // 如果没有找到配置文件，默认版本号为1.0.0
        return bValue;
      }
    }

    /// <summary>
    /// 更新目录
    /// </summary>
    static string UpdateDirectory {
      get {
        var bDir = GetConfigValue("Settings", "UpdateDirectory") ?? "patches"; // 如果没有找到配置文件，默认更新目录为patches
        if (!Directory.Exists(bDir)) {
          Directory.CreateDirectory(bDir);
        }
        return bDir;
      }
    }

    /// <summary>
    /// 获取下一个要更新的版本
    /// </summary>
    /// <param name="aCurrentVersion">当前版本</param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    Version GetNextVersion(Version aCurrentVersion) {
      Version? bNextVersion = null;
      foreach (var bVersion in fAllVersions!) {
        if (bVersion > aCurrentVersion) {
          bNextVersion = bVersion;
          break;
        }
      }

      if (bNextVersion == null) {
        throw new InvalidOperationException($"错误：找不到基于【{aCurrentVersion}】的下一个版本信息。");
      }
      else {
        return bNextVersion;
      }
    }

    /// <summary>
    /// 更新至最新版本
    /// </summary>
    /// <param name="aLocalVersion">本地版本</param>
    /// <param name="aTargetVersion">目标版本</param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    async Task UpdateToLatestVersion(string aLocalVersion, Version aTargetVersion) {
      var bCurrentVersion = new Version(aLocalVersion);

      while (bCurrentVersion < aTargetVersion) {
        var bNextVersion = GetNextVersion(bCurrentVersion);
        var bPatchUrl = await GetPatchUrlForVersion(bNextVersion);
        var bZipFile = await DownloadPatch(bPatchUrl);
        ExtractZipFile(bZipFile, Application.StartupPath);
        UpdateVersion(bNextVersion);
        bCurrentVersion = bNextVersion;
      }

      if (bCurrentVersion == aTargetVersion) {
        MessageBox.Show("程序已更新，请重启客户端", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
      }
    }

    /// <summary>
    /// 根据URL下载更新文件
    /// </summary>
    /// <param name="aPatchUrl"></param>
    /// <returns>返回zip文件路径</returns>
    static async Task<string> DownloadPatch(string aPatchUrl) {
      try {
        string bFileName = $"patch_{DateTime.Now:yyyyMMddHHmmss}.zip"; // 默认下载的文件名称
        HttpResponseMessage bResponse = await HttpClientSingleton.Instance.GetAsync(aPatchUrl);
        bResponse.EnsureSuccessStatusCode();
        var bContentDisposition = bResponse.Content.Headers.ContentDisposition;
        if (bContentDisposition != null) {
          bFileName = bContentDisposition.FileNameStar ?? bContentDisposition.FileName ?? bFileName;
        }
        string bFilePath = Path.Combine(UpdateDirectory, bFileName);

        // 下面这种using语句写法是从C# 8.0开始出现；fileStream 之所以省略了FileStream声明，是因为在using开头声明了Stream。
        // 而且FileStream是Stream的子类，这种条件下才可以这样使用。
        using (Stream contentStream = await bResponse.Content.ReadAsStreamAsync(),
               fileStream = new FileStream(bFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) {
          await contentStream.CopyToAsync(fileStream);
        }

        return bFilePath;
      }
      catch (Exception ex) {
        throw new Exception($"{ex.Message}，请求URL：{aPatchUrl}");
      }
    }

    /// <summary>
    /// 获取对应版本补丁的下载URL
    /// </summary>
    /// <param name="aVersion">版本</param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    static async Task<string> GetPatchUrlForVersion(Version aVersion) {
      var bServerUrl = GetConfigValue("Settings", "ServerUrl");
      var bRequestApi = $"{bServerUrl}/patch/{aVersion}";
      try {
        var bResponse = await HttpClientSingleton.Instance.GetAsync(bRequestApi);
        if (bResponse.IsSuccessStatusCode) {
          return await bResponse.Content.ReadAsStringAsync();
        }
        else {
          throw new FileNotFoundException($"补丁资源未找到，请求URL：{bRequestApi}");
        }
      }
      catch (FileNotFoundException) {
        throw;
      }
      catch (Exception ex) {
        throw new Exception($"{ex.Message}，请求URL：{bRequestApi}");
      }
    }

    /// <summary>
    /// 获取所有的版本
    /// </summary>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    static async Task<List<Version>?> GetAllVersions() {
      var bServerUrl = GetConfigValue("Settings", "ServerUrl");
      var bRequestUrl = $"{bServerUrl}/versions";
      try {
        var bResponse = await HttpClientSingleton.Instance.GetAsync(bRequestUrl);
        bResponse.EnsureSuccessStatusCode();
        var bContent = await bResponse.Content.ReadAsStringAsync();
        var bVersions = JsonSerializer.Deserialize<List<string>>(bContent);
        var bVersionObjects = bVersions?.Select(v => new Version(v)).OrderBy(v => v).ToList();
        return bVersionObjects;
      }
      catch (Exception ex) {
        throw new Exception(ex.Message + $"，请求URL：{bRequestUrl}");
      }
    }

    /// <summary>
    /// 获取安全路径
    /// </summary>
    /// <param name="aBasePath">基础路径</param>
    /// <param name="aRelativePath">相对路径</param>
    /// <returns></returns>
    /// <exception cref="IOException"></exception>
    /// <remarks>
    /// 使用 Path.Combine 将 basePath 和 relativePath 组合成一个完整路径。
    /// 使用 Path.GetFullPath 将组合后的路径转换为绝对路径，解析任何.（当前目录）或..（上级目录）等相对路径符号。
    /// 检查生成的完整路径（fullPath）是否以 basePath 开头（忽略大小写）。
    /// 如果是，说明路径是安全的，返回该完整路径。
    /// 如果不是，说明路径可能试图跳出 basePath 的范围，抛出 IOException 异常
    /// 
    /// 通过检查fullPath是否以basePath开头，确保生成的路径不会访问或者写入basePath之外的目录。
    /// 这是一种常见的防御路径遍历攻击的手段。
    /// 
    /// 举例：
    /// basePath 是 C:\Projects\MyApp。
    /// relativePath 是 ..\..\Windows\System32\cmd.exe。
    /// Path.Combine(basePath, relativePath) 会生成 C:\Projects\MyApp\..\..\Windows\System32\cmd.exe。
    /// Path.GetFullPath 会将其解析为 C:\Windows\System32\cmd.exe。
    /// 检查 C:\Windows\System32\cmd.exe 是否以 C:\Projects\MyApp 开头，显然不是，因此会抛出异常。
    /// </remarks>
    static string GetSafeFilePath(string aBasePath, string aRelativePath) {
      string bFullPath = Path.GetFullPath(Path.Combine(aBasePath, aRelativePath));
      if (!bFullPath.StartsWith(aBasePath, StringComparison.OrdinalIgnoreCase)) {
        throw new IOException("试图提取到归档目录之外的路径是不允许的。");
      }
      return bFullPath;
    }

    static void ExtractZipFile(string aFilePath, string aTargetFolder) {
      try {
        using var bZipFileOpen = new FileStream(aFilePath, FileMode.Open);
        using var bZipArchive = new ZipArchive(bZipFileOpen, ZipArchiveMode.Read, false, System.Text.Encoding.UTF8);
        foreach (var bZipArchiveEntry in bZipArchive.Entries) {
          // 检索文件，如果当前的对象是个目录就跳过检查下一个
          if (bZipArchiveEntry.FullName.EndsWith("/", StringComparison.Ordinal)) {
            continue;
          }
          var bEntryFullPath = GetSafeFilePath(aTargetFolder, bZipArchiveEntry.FullName);

          var bEntryDirectory = Path.GetDirectoryName(bEntryFullPath)!;

          // 确保目录存在，这么做是为了，当文件是包含这目录的时候，执行bZipArchiveEntry.ExtractToFile语句，确保目录是存在的，否则会报错。
          if (!Directory.Exists(bEntryDirectory)) {
            Directory.CreateDirectory(bEntryDirectory);
          }

          // 对文件重命名备份
          RenameFileForBak(bEntryFullPath);

          // 因为上面已经保证了文件的重命名，所以这个方法的第二个参数false或true都是可以的。（false的话，如果遇到名字一样的，就会报异常错误）
          bZipArchiveEntry.ExtractToFile(bEntryFullPath, false);
        }
      }
      catch (Exception ex) {
        throw new Exception($"{ex.Message}, 文件：{aFilePath}");
      }
    }

    /// <summary>
    /// 读取配置文件
    /// </summary>
    /// <param name="aFilePath"></param>
    /// <returns></returns>
    static Dictionary<string, Dictionary<string, string>> ReadIniConfig(string aFilePath) {
      var bConfig = new Dictionary<string, Dictionary<string, string>>();
      string? bCurrentSection = null;

      try {
        foreach (string bLine in File.ReadAllLines(aFilePath)) {
          string bTrimmedLine = bLine.Trim();
          if (bTrimmedLine.StartsWith("[") && bTrimmedLine.EndsWith("]")) {
            bCurrentSection = bTrimmedLine.Substring(1, bTrimmedLine.Length - 2);
            bConfig[bCurrentSection] = new Dictionary<string, string>();
          }
          else if (!string.IsNullOrEmpty(bTrimmedLine) && bTrimmedLine.Contains("=")) {
            if (bCurrentSection != null) {
              string[] bParts = bTrimmedLine.Split('=', 2);
              string bKey = bParts[0].Trim();
              string bValue = bParts[1].Trim();
              bConfig[bCurrentSection][bKey] = bValue;
            }
          }
        }
      }
      catch (FileNotFoundException ex) {
        LogMessage($"配置文件 {aFilePath} 不存在: {ex.Message}");
      }
      catch (IOException ex) {
        LogMessage($"读取配置文件 {aFilePath} 时出错: {ex.Message}");
      }
      return bConfig;
    }

    /// <summary>
    /// 获取相应节点的配置内容
    /// </summary>
    /// <param name="aConfig">配置信息</param>
    /// <param name="aSection">节点</param>
    /// <param name="aKey">Key</param>
    /// <returns>注意：可能返回NULL</returns>
    static string? GetConfigValue(string aSection, string aKey) {
      var bConfig = ReadIniConfig(fConfigName);
      if (bConfig != null) {
        if (bConfig.TryGetValue(aSection, out var bSectionConfig)
          && bSectionConfig.TryGetValue(aKey, out var bValue)) {
          return bValue;
        }
      }
      return null;
    }

    /// <summary>
    /// 更新版本号
    /// </summary>
    /// <param name="aVersion"></param>
    static void UpdateVersion(Version aVersion) {
      var bConfig = ReadIniConfig(fConfigName);
      bConfig["Settings"]["CurrentVersion"] = aVersion.ToString();

      using (StreamWriter bWriter = new StreamWriter(fConfigName)) {
        if (bConfig != null) {
          foreach (var bSection in bConfig) {
            bWriter.WriteLine($"[{bSection.Key}]");
            foreach (var bKeyValue in bSection.Value) {
              bWriter.WriteLine($"{bKeyValue.Key}={bKeyValue.Value}");
            }
            bWriter.WriteLine();
          }
        }
      }
    }

    /// <summary>
    /// 对文件进行备份重命名
    /// </summary>
    /// <param name="aEntryFullPath"></param>
    static void RenameFileForBak(string aEntryFullPath) {
      if (File.Exists(aEntryFullPath)) {
        var bTimestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
        var bFileExtension = Path.GetExtension(aEntryFullPath);
        var bNewFileName = Path.ChangeExtension(aEntryFullPath, $"{bFileExtension}_{bTimestamp}.bak");
        File.Move(aEntryFullPath, bNewFileName);
      }
      else {

      }
    }

    /// <summary>
    /// 记录日志
    /// </summary>
    /// <param name="aMessage">记录日志信息</param>
    /// <remarks>
    /// 记录日志里面不能在去读配置文件，否则的话，就可能会造成无限循环错误。
    /// 注意：该方法是public
    /// </remarks>
    public static void LogMessage(string aMessage) {

      if (!Directory.Exists(fLogDir)) {
        Directory.CreateDirectory(fLogDir);
      }

      // 日志文件每天一个
      string bLogFileName = $"logs/app_{DateTime.Now:yyyy-MM-dd}.log";

      // 构建具体的日志记录信息
      string bLogEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss}-{aMessage}";

      try {
        File.AppendAllText(bLogFileName, bLogEntry + Environment.NewLine);
      }
      catch (UnauthorizedAccessException ex) {
        MessageBox.Show($"写入日志权限不足: {ex.Message}");
      }
      catch (ArgumentException ex) {
        MessageBox.Show($"写入日志文件路径无效: {ex.Message}");
      }
      catch (DirectoryNotFoundException ex) {
        MessageBox.Show($"写入日志目录不存在: {ex.Message}");
      }
      catch (IOException ex) {
        MessageBox.Show($"写入日志文件时发生 I/O 错误: {ex.Message}");
      }
      catch (Exception ex) {
        MessageBox.Show($"写入日志文件时发生未知错误: {ex.Message}");
      }
    }
  }
}
